package cn.seaboot.admin.security.login;

import cn.seaboot.commons.digest.Hex;
import cn.seaboot.commons.digest.MD5;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.security.SecureRandom;

/**
 * 加盐值算法的{@link BCryptPasswordEncoder}
 * <p>
 * 在{@link BCryptPasswordEncoder}的基础上，增加了盐值的相关设计，
 * 参考了Shiro的设计，不知道算法的前提下，杜绝伪造密码的隐患。
 * <p>
 * PasswordHelper helper = new PasswordHelper();
 * // 原始密码
 * String password = "test";
 * // 密码盐值
 * String salt = helper.createPasswordSalt();
 * // 加密后的密码
 * String digest = helper.getPasswordDigest(password, salt);
 * // bCryptPassword加密密码
 * String bCryptPassword = helper.getBCryptPassword(digest, salt);
 * <p>
 * // 密码可以根据bCryptPassword进行匹配计算
 * System.out.println(helper.matches(password, bCryptPassword));
 * System.out.println(helper.matches(password, digest, salt));
 * System.out.println(bCryptPassword);
 *
 * @author Mr.css
 * @version 2020-12-09 15:39
 */
@Configuration
public class PasswordHelper implements PasswordEncoder {
    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

//    public static void main(String[] args) {
//        PasswordHelper helper = new PasswordHelper();
//// 需要加密的原始密码
//        String password = "test";
////        3e7b7dd0a8507b9a0aa67a61fe3b0265
////        37f4161dee71a8e9c07834e2d81cb693
//// 密码盐值
//        String salt = helper.createPasswordSalt();
//        System.out.println(salt);
//// 加密后的密码
//        String digest = helper.getPasswordDigest(password, salt);
//        System.out.println(digest);
//    }

    /**
     * 生成随机盐值
     *
     * @return 盐值
     */
    public String createPasswordSalt() {
        return Hex.encodeToString(this.randomByte());
    }

    /**
     * 生成随机盐值
     *
     * @param seed 种子
     * @return 盐值
     */
    public String createPasswordSalt(String seed) {
        byte[] sd = MD5.hexBit16(seed).getBytes();
        byte[] bytes = this.randomByte();
        return Hex.encodeToString(xor(bytes, sd));
    }

    /**
     * 构造16位随机数
     *
     * @return bytes
     */
    private byte[] randomByte() {
        byte[] bytes = new byte[16];
        new SecureRandom().nextBytes(bytes);
        return bytes;
    }

    /**
     * 使用盐值，对原始密码进行加密。
     * <p>
     * 计算方式类似于摘要运算，原始密码与盐值进行摘要运算结果后，
     * 会产生唯一结果，数据库不存储密码本身，只保留运算的结果。
     * <p>
     * 密码验证的时候，使用密码与盐值，进行相同的摘要运算，
     * 如果与结果匹配，则说明密码正确。
     *
     * @param password 原始密码
     * @param salt     盐值
     * @return 校验码（密码摘要）
     */
    public String getPasswordDigest(String password, String salt) {
        byte[] pwd = MD5.digest(password.getBytes());
        byte[] salts = Hex.decode(salt);
        return Hex.encodeToString(xor(pwd, salts));
    }

    /**
     * 通过密码摘要和盐值，生成BCrypt算法所需的密钥。
     *
     * @param digest 密码摘要
     * @param salt   盐值
     * @return BCryptPassword加密的密码
     */
    public String getBCryptPassword(String digest, String salt) {
        byte[] pwd = Hex.decode(digest);
        byte[] salts = Hex.decode(salt);
        byte[] md = xor(pwd, salts);
        String res = new String(md);
        return passwordEncoder.encode(res);
    }

    /**
     * 密码匹配验证
     *
     * @param password 原始密码
     * @param salt     盐值
     * @param digest   校验码（密码摘要）
     * @return boolean
     */
    public boolean matches(String password, String salt, String digest) {
        String ret = getPasswordDigest(password, salt);
        return digest.equals(ret);
    }

    /**
     * 对字符串进行加密，生成bCryptString
     *
     * @param password 原始密码
     * @return bCryptString
     * @see PasswordEncoder
     */
    @Override
    public String encode(CharSequence password) {
        byte[] str = MD5.digest(String.valueOf(password).getBytes());
        return passwordEncoder.encode(new String(str));
    }

    /**
     * 字符匹配验证
     *
     * @param password     需要验证的字符串
     * @param bCryptString 经过encode函数加密的字符串
     * @return boolean
     * @see PasswordEncoder
     */
    @Override
    public boolean matches(CharSequence password, String bCryptString) {
        byte[] str = MD5.digest(String.valueOf(password).getBytes());
        return passwordEncoder.matches(new String(str), bCryptString);
    }

    /**
     * 密码与盐值进行合并重组
     *
     * @param pwd   密码
     * @param salts 盐值
     * @return 加密之后的密码
     */
    private byte[] xor(byte[] pwd, byte[] salts) {
        for (int i = 0; i < pwd.length; i++) {
            pwd[i] = (byte) (pwd[i] ^ salts[i]);
        }
        return pwd;
    }
}
